/*
* @author Ali Ok / Oleg Varaksin
* $$Id: AbstractProcessMojo.java 50 2011-01-29 00:24:11Z mcaliskan $$
*/

package com.googlecode.jsf2inspector.plugin.commons;

import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.util.DirectoryScanner;
import org.codehaus.plexus.util.FileUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.*;

public abstract class AbstractProcessMojo<T> extends AbstractMojo
{
    private DocumentBuilder docBuilder;

    private Set<File> facesConfigFiles;

    protected List<Inspector<T>> inspectors = new ArrayList<Inspector<T>>();

    /**
     * The Maven project.
     *
     * @parameter default-value="${project}"
     * @required
     * @readonly
     */
    protected MavenProject project;

    /**
     * Directory containing compiled classes.
     *
     * @parameter default-value="${project.build.outputDirectory}"
     * @required
     * @readonly
     */
    protected File classesDirectory;

    /**
     * Source webapp directory.
     *
     * @parameter expression="${inspector.webappDir}" default-value="${project.basedir}/src/main/webapp"
     */
    protected File webappDirectory;

    /**
     * web.xml file.
     *
     * @parameter expression="${inspector.webXml}" default-value="${project.basedir}/src/main/webapp/WEB-INF/web.xml"
     */
    protected File webXml;

    public void execute() throws MojoExecutionException, MojoFailureException {
        getLog().info("Inspect files.");
        getLog().info("----::" + inspectors.size());
        try {
            for (Inspector<T> inspector : inspectors) {
                System.out.println("----::" + inspector.getClass());
                getLog().info("----::" + inspector.getClass());
                Set<T> objects = inspector.getObjectsToInspect();
                boolean foundError = false;

                for (T object : objects) {
                    getLog().info("Processing object " + object.toString() + " ...");
                    List<InspectionMessage> messageList = inspector.inspect(object);
                    if (messageList != null && !messageList.isEmpty()) {
                        for (InspectionMessage message : messageList) {
                            String strMessage = message.getMessage() + " at " + message.getFile();
                            if (inspector.isFailOnWarning()) {
                                getLog().error(strMessage);
                                foundError = true;
                            } else {
                                getLog().warn(strMessage);
                            }
                        }
                    }

                    if (foundError) {
                        throw new MojoExecutionException("jsf2-inspector plugin : see Maven error logs.");
                    }
                }
            }
        } catch (MojoExecutionException e) {
            getLog().error(e.getMessage());
            throw e;
        } catch (Exception e) {
            getLog().error(e.getMessage());
            throw new MojoExecutionException("Error while executing the mojo " + getClass(), e);
        }

        getLog().info("Files successfully inspected.");
    }

    @SuppressWarnings("unchecked")
    public ClassLoader getClassLoader() throws MojoExecutionException {
        URL[] urls;

        try {
            Collection<Artifact> artefacts = project.getArtifacts();
            File[] files = new File[artefacts.size() + 1];
            files[0] = classesDirectory;

            int i = 1;
            for (Artifact artefact : artefacts) {
                files[i] = artefact.getFile();
                i++;
            }
            urls = FileUtils.toURLs(files);
        } catch (Exception e) {
            throw new MojoExecutionException("Classloader could not be acquired", e);
        }

        return new URLClassLoader(urls, Thread.currentThread().getContextClassLoader());
    }


    public DocumentBuilder getDocumentBuilder() throws MojoExecutionException {
        if (docBuilder != null) {
            return docBuilder;
        }

        DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
        docBuilderFactory.setIgnoringElementContentWhitespace(true);
        docBuilderFactory.setValidating(false);
        try {
            docBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
        } catch (ParserConfigurationException e) {
            throw new MojoExecutionException("Unable to set feature", e);
        }

        try {
            docBuilder = docBuilderFactory.newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            throw new MojoExecutionException("Unable to create document builder.", e);
        }

        return docBuilder;
    }

    public Set<File> getFacesConfigFiles() throws MojoExecutionException {
        if (facesConfigFiles != null) {
            return facesConfigFiles;
        }

        facesConfigFiles = new HashSet<File>();

        // Find all configuration files according to the JSF 2 spec. section 11.4.2.
        // 1. Search for all resources that match either "META-INF/faces-config.xml"
        // or end with ".faces-config.xml" directly in the "META-INF" directory.
        // 2. Check for the existence of a context initialization parameter named javax.faces.CONFIG_FILES.
        // If it exists, treat it as a comma-delimited list of context relative resource paths (starting with a "/").

        // Step 1
        DirectoryScanner directoryScanner = new DirectoryScanner();
        directoryScanner.setBasedir(webappDirectory);
        String[] includes = {"META-INF/faces-config.xml", "META-INF/*.faces-config.xml"};
        directoryScanner.setIncludes(includes);

        try {
            getLog().debug("Scanning faces config files under META-INF ...");
            directoryScanner.scan();
        } catch (IllegalStateException e) {
            throw new MojoExecutionException("Error while scanning faces config files under META-INF", e);
        }

        for (String fileName : directoryScanner.getIncludedFiles()) {
            File configFile = new File(webappDirectory, fileName);
            facesConfigFiles.add(configFile);
        }

        // Step 2
        DocumentBuilder documentBuilder = getDocumentBuilder();
        Document doc;

        try {
            getLog().debug("Parsing web.xml file " + webXml.toString() + " ...");
            doc = documentBuilder.parse(webXml);
        } catch (Exception e) {
            throw new MojoExecutionException("Error while parsing web.xml file: " + webXml.toString(), e);
        }

        NodeList contextParams = doc.getElementsByTagName("context-param");
        if (contextParams == null || contextParams.getLength() < 1) {
            return facesConfigFiles;
        }

        Node configFilesNode = null;
        for (int i = 0; i < contextParams.getLength(); i++) {
            Node contextParam = contextParams.item(i);
            NodeList childs = contextParam.getChildNodes();
            for (int j = 0; j < childs.getLength(); j++) {
                Node child = childs.item(j);
                if ("param-name".equals(child.getNodeName()) && "javax.faces.CONFIG_FILES".equals(child.getTextContent().trim())) {
                    configFilesNode = contextParam;
                    break;
                }
            }
        }

        if (configFilesNode == null) {
            return facesConfigFiles;
        }

        NodeList childs = configFilesNode.getChildNodes();
        for (int j = 0; j < childs.getLength(); j++) {
            Node child = childs.item(j);
            if ("param-value".equals(child.getNodeName())) {
                String strConfigFiles = child.getTextContent().trim();
                if (StringUtils.isNotBlank(strConfigFiles)) {
                    String[] arrConfigFiles = strConfigFiles.split(",");
                    if (ArrayUtils.isNotEmpty(arrConfigFiles)) {
                        for (String strConfigFile : arrConfigFiles) {
                            if (StringUtils.isNotBlank(strConfigFile)) {
                                File configFile = new File(webappDirectory, strConfigFile.trim());
                                facesConfigFiles.add(configFile);
                            }
                        }
                    }
                }
                break;
            }
        }

        return facesConfigFiles;
    }

    public void addInspector(Inspector<T> inspector) {
        inspector.setProcessMojo(this);
        inspectors.add(inspector);
    }

    public MavenProject getProject() {
        return project;
    }
}
